package org.checkerframework.framework.test;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.annotation.processing.AbstractProcessor;
import org.checkerframework.checker.signature.qual.BinaryName;
import org.junit.Test;
import org.junit.runner.RunWith;

/**
 * Compiles all test files in a test directory together. Use {@link CheckerFrameworkPerFileTest} to
 * compile each test file in a test directory individually. A {@link
 * CheckerFrameworkPerDirectoryTest} is faster than an equivalent {@link
 * CheckerFrameworkPerFileTest}, but can only test that processor errors or warnings are issued.
 *
 * <p>To create a {@link CheckerFrameworkPerDirectoryTest}, create a new class that extends this
 * class. The new class must do the following:
 *
 * <ol>
 *   <li>Declare a constructor taking 1 parameter of type {@code java.util.List<java.io.File>}. This
 *       is a list of the files that will be compiled.
 *   <li>Declare the following method:
 *       <pre>{@code @Parameters public static String [] getTestDirs()}</pre>
 *       <p>getTestDir must return an array of directories that exist in the test folder. The
 *       directories can contain more path information (e.g., "myTestDir/moreTests") but note, the
 *       test suite will find all of the Java test files that exists below the listed directories.
 *       It is unnecessary to list child directories of a directory you have already listed.
 * </ol>
 *
 * Here is an example:
 *
 * <pre><code>
 * public class MyTest extends CheckerFrameworkPerDirectoryTest {
 *   /** {@literal @}param testFiles the files containing test code, which will be type-checked *{@literal /}
 *   public MyTest(List{@literal <}File{@literal >} testFiles) {
 *     super(testFiles, MyChecker.class, "", "Anomsgtext");
 *   }
 *  {@literal @}Parameters
 *   public static String [] getTestDirs() {
 *     return new String[] {"all-systems"};
 *   }
 * }
 * </code></pre>
 */
@RunWith(PerDirectorySuite.class)
public abstract class CheckerFrameworkPerDirectoryTest extends CheckerFrameworkRootedTest {

  /** The files containing test code, which will be type-checked. */
  protected final List<File> testFiles;

  /** The binary names of the checkers to run. */
  protected final List<@BinaryName String> checkerNames;

  /**
   * The path, relative to the test root directory (see {@link
   * CheckerFrameworkRootedTest#resolveTestDirectory()}), to the directory containing test inputs.
   */
  protected final String testDir;

  /** Extra options to pass to javac when running the checker. */
  protected final List<String> checkerOptions;

  /** Extra entries for the classpath. */
  protected final List<String> classpathExtra;

  /**
   * Creates a new checker test.
   *
   * <p>{@link TestConfigurationBuilder#getDefaultConfigurationBuilder(String, File, String,
   * Iterable, Iterable, List, boolean)} adds additional checker options.
   *
   * @param testFiles the files containing test code, which will be type-checked
   * @param checker the class for the checker to use
   * @param testDir the path, relative to currentDir/tests, to the directory of test inputs
   * @param checkerOptions options to pass to the compiler when running tests
   */
  protected CheckerFrameworkPerDirectoryTest(
      List<File> testFiles,
      Class<? extends AbstractProcessor> checker,
      String testDir,
      String... checkerOptions) {
    this(testFiles, checker, testDir, Collections.emptyList(), checkerOptions);
  }

  /**
   * Creates a new checker test.
   *
   * <p>{@link TestConfigurationBuilder#getDefaultConfigurationBuilder(String, File, String,
   * Iterable, Iterable, List, boolean)} adds additional checker options.
   *
   * @param testFiles the files containing test code, which will be type-checked
   * @param checker the class for the checker to use
   * @param testDir the path, relative to currentDir/tests, to the directory of test inputs
   * @param classpathExtra extra entries for the classpath, relative to a directory such as
   *     checker-framework/checker
   * @param checkerOptions options to pass to the compiler when running tests
   */
  @SuppressWarnings(
      "signature:cast.unsafe" // for non-array non-primitive class, getName(): @BinaryName
  )
  protected CheckerFrameworkPerDirectoryTest(
      List<File> testFiles,
      Class<? extends AbstractProcessor> checker,
      String testDir,
      List<String> classpathExtra,
      String... checkerOptions) {
    this(
        testFiles,
        Collections.singletonList((@BinaryName String) checker.getName()),
        testDir,
        classpathExtra,
        checkerOptions);
  }

  /**
   * Creates a new checker test.
   *
   * <p>{@link TestConfigurationBuilder#getDefaultConfigurationBuilder(String, File, String,
   * Iterable, Iterable, List, boolean)} adds additional checker options.
   *
   * @param testFiles the files containing test code, which will be type-checked
   * @param checkerNames the binary names of the checkers to run
   * @param testDir the path, relative to currentDir/tests, to the directory of test inputs
   * @param classpathExtra extra entries for the classpath, relative to a directory such as
   *     checker-framework/checker
   * @param checkerOptions options to pass to the compiler when running tests
   */
  protected CheckerFrameworkPerDirectoryTest(
      List<File> testFiles,
      List<@BinaryName String> checkerNames,
      String testDir,
      List<String> classpathExtra,
      String... checkerOptions) {
    super();
    this.testFiles = testFiles;
    this.checkerNames = checkerNames;
    this.testDir = testDir;
    this.classpathExtra = classpathExtra;
    this.checkerOptions = new ArrayList<>(Arrays.asList(checkerOptions));
    this.checkerOptions.add("-AajavaChecks");
    this.checkerOptions.add("-AconvertTypeArgInferenceCrashToWarning=false");
  }

  /** Run the tests. */
  @Test
  public void run() {
    if (testFiles.isEmpty()) {
      return;
    }
    boolean shouldEmitDebugInfo = TestUtilities.getShouldEmitDebugInfo();
    List<String> customizedOptions = customizeOptions(Collections.unmodifiableList(checkerOptions));
    TestConfiguration config =
        TestConfigurationBuilder.buildDefaultConfiguration(
            new File(resolveTestDirectory(), testDir).getPath(),
            testFiles,
            classpathExtra,
            checkerNames,
            customizedOptions,
            shouldEmitDebugInfo);
    TypecheckResult testResult = new TypecheckExecutor().runTest(config);
    TypecheckResult adjustedTestResult = adjustTypecheckResult(testResult);
    checkResult(adjustedTestResult);
  }

  /**
   * This method is called before issuing assertions about a TypecheckResult. Subclasses can
   * override it to customize behavior.
   *
   * @param testResult a test result to possibly change
   * @return a TypecheckResult to use instead, which may be the unmodified argument
   */
  public TypecheckResult adjustTypecheckResult(TypecheckResult testResult) {
    return testResult;
  }

  /**
   * Override this method if you would like to supply a checker command-line option that depends on
   * the Java files passed to the test. Those files are available in field {@link #testFiles}.
   *
   * <p>If you want to specify the same command-line option for all tests of a particular checker,
   * then pass it to the {@link #CheckerFrameworkPerDirectoryTest} constructor.
   *
   * @param previousOptions the options specified in the constructor of the test previousOptions is
   *     unmodifiable
   * @return a new list of options or the original passed through
   */
  public List<String> customizeOptions(List<String> previousOptions) {
    return previousOptions;
  }
}
